/*
 *
 * Copyright (C) 2012 Walter Lütgenau
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */
package offset.nodes.server.model;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.Calendar;
import java.util.StringTokenizer;
import javax.jcr.ItemNotFoundException;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.Property;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.Workspace;
import javax.jcr.nodetype.NoSuchNodeTypeException;
import javax.jcr.nodetype.NodeType;
import javax.jcr.query.Query;
import javax.jcr.query.QueryManager;
import javax.jcr.query.QueryResult;
import javax.servlet.http.HttpServletRequest;
import offset.nodes.Constants;
import offset.nodes.server.binary.controller.BinaryAttribute;
import offset.nodes.client.model.SimpleNamespaceRegistry;
import offset.nodes.server.controller.AttributeConstants;
import org.apache.jackrabbit.core.SessionImpl;
import org.apache.jackrabbit.core.nodetype.NodeTypeDef;
import org.apache.jackrabbit.core.nodetype.NodeTypeManagerImpl;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl;

/**
 *
 * @author Walter Lütgenau
 */
public class RepositoryModel {

    private Session session;
    SimpleNamespaceRegistry namespaces = SimpleNamespaceRegistry.getInstance();

    /** Creates a new instance of PageModel
     * @param session session provided by the caller
     */
    public RepositoryModel(Session session) {
        this.session = session;
    }

    public SimpleNamespaceRegistry getNamespaces() {
        return namespaces;
    }

   public static Session getRepositorySession(HttpServletRequest request) {
        return (Session) request.getSession().getAttribute(AttributeConstants.ATT_REPOSITORY_SESSION);
   }

    public NodeIterator executeQuery(String queryString) throws RepositoryException {
        if (queryString.startsWith("/"))
            queryString = "/jcr:root" + queryString;
        Workspace workspace = getSession().getWorkspace();
        QueryManager manager = workspace.getQueryManager();

        Query query = manager.createQuery(queryString, Query.XPATH);
        QueryResult result = query.execute();

        return result.getNodes();

    }

    /**
     * Remove a node. To do this, all mixin types must be removed at first.
     *
     * @param path The path to the node to be removed
     * @throws RepositoryException
     */
    public void remove(String path) throws RepositoryException {
        Node root = getSession().getRootNode();
        Node node = root.getNode(normalizePath(path));

        if (node.getReferences().getSize() > 0)
            throw new RuntimeException("There are references");

        NodeType[] mixin = node.getMixinNodeTypes();
        for (int k = 0; k < mixin.length; k++) {
            node.removeMixin(mixin[k].getName());
        }

        if (node != null) {
            node.remove();
            getSession().save();
        }
    }

    /**
     * Remove a specific mixin type
     *
     * @param path The node with the mixin type
     * @param mixinType The mixin type
     * @throws RepositoryException
     */
    public void removeMixin(String path, String mixinType) throws RepositoryException {
        Node root = getSession().getRootNode();
        Node node = root.getNode(normalizePath(path));

        if (!node.isNodeType(mixinType))
            return;
        node.removeMixin(mixinType);

        getSession().save();
    }

    public Calendar getFileModificationTime(String path) throws RepositoryException {
        while (path.startsWith(Constants.PATH_SEPARATOR)) {
            path = path.substring(1);
        }

        Node root = getSession().getRootNode();
        Node pageNode = root.getNode(path + Constants.PATH_SEPARATOR + Constants.JCR_CONTENT);
        if (pageNode == null)
            return null;

        Calendar modificationTime = null;
        if (pageNode.hasProperty(Constants.PROP_LAST_MODIFIED))
            modificationTime = pageNode.getProperty(Constants.PROP_LAST_MODIFIED).getDate();

        return modificationTime;
    }

    public BinaryAttribute getFile(String path) throws RepositoryException {
        while (path.startsWith(Constants.PATH_SEPARATOR)) {
            path = path.substring(1);
        }

        Node root = getSession().getRootNode();
        Node pageNode = root.getNode(path +  Constants.PATH_SEPARATOR + Constants.JCR_CONTENT);
        if (pageNode == null)
            return null;

        String contentType = null;
        if (pageNode.hasProperty(Constants.PROP_MIME_TYPE))
            contentType = pageNode.getProperty(Constants.PROP_MIME_TYPE).getValue().getString();
        InputStream input = pageNode.getProperty(Constants.PROP_DATA).getValue().getStream();

        BinaryAttribute info = new BinaryAttribute();
        info.setContentType(contentType);
        info.setInput(input);

        return info;
    }

    public String getFileAsString(String path) throws RepositoryException {
        while (path.startsWith(Constants.PATH_SEPARATOR)) {
            path = path.substring(1);
        }

        Node root = getSession().getRootNode();
        Node pageNode = root.getNode(path +  Constants.PATH_SEPARATOR + Constants.JCR_CONTENT);
        if (pageNode == null)
            return null;

        String contentType = null;
        if (pageNode.hasProperty(Constants.PROP_MIME_TYPE))
            contentType = pageNode.getProperty(Constants.PROP_MIME_TYPE).getValue().getString();
        String input = pageNode.getProperty(Constants.PROP_DATA).getValue().getString();

        return input;
    }

    public BinaryAttribute getDownloadInfo(Node node) throws RepositoryException {
        if (node == null)
            return null;

        BinaryAttribute info = new BinaryAttribute();
        Calendar modification = null;
        if (node.hasProperty(Constants.PROP_LAST_MODIFIED)) // TODO: remove this after converting repository content
            modification = node.getProperty(Constants.PROP_LAST_MODIFIED).getDate();
        else if (node.hasProperty(Constants.PROP_MODIFICATION_TIMESTAMP))
            modification = node.getProperty(Constants.PROP_MODIFICATION_TIMESTAMP).getDate();
        if (modification != null)
            info.setModification(modification.getTime());
        info.setName(node.getName());

        Node pageNode = null;
        if (node.hasNode(Constants.NODES_BODY))
            pageNode = node.getNode(Constants.NODES_BODY + Constants.PATH_SEPARATOR + Constants.JCR_CONTENT); // historical reasons. TODO: modify repository and discard this line
        else
            pageNode = node.getNode(Constants.JCR_CONTENT);
        if (pageNode == null)
            return null;

        String contentType = null;
        if (pageNode.hasProperty(Constants.PROP_MIME_TYPE))
            contentType = pageNode.getProperty(Constants.PROP_MIME_TYPE).getValue().getString();

        if (!pageNode.hasProperty(Constants.JCR_DATA))
            return null;

        InputStream input = pageNode.getProperty(Constants.JCR_DATA).getValue().getStream();

        info.setInput(input);
        info.setContentType(contentType);
        return info;
    }

    public String normalizePath(String path) {
        if (path == null)
            return "";
        while (path.startsWith("/")) {
            path = path.substring(1);
        }

        return path;
    }

    public Node getReferencedNode(String reference) throws RepositoryException {
        try {
            Node result = getSession().getNodeByUUID(reference);
            return result;
        } catch (RepositoryException e) {
            return null;
        }
    }

    public Node getNodeByUuid(String uuid) throws RepositoryException {
        return getSession().getNodeByUUID(uuid);
    }

    public Node getNode(String path) throws RepositoryException {
        path = normalizePath(path);

        Node root = getSession().getRootNode();
        if (path.length() == 0)
            return root;

        if (!root.hasNode(path))
            return null;

        Node pageNode = root.getNode(path);

        return pageNode;
    }

    /**
     * Return a node with a relative path to the root. Create path nodes, if necessory
     *
     * @param root
     * @param path
     * @return
     * @throws javax.jcr.RepositoryException
     */
    public Node getNode(Node root, String path) throws RepositoryException {
        if (path == null)
            return null;

        if (path.startsWith("/")) // only pathes relative to root allowed
            path = path.substring(1);

        StringTokenizer st = new StringTokenizer(path, "/");
        Node node = root;

        while (st.hasMoreTokens()) {
            String component = st.nextToken();

            Node componentNode = null;
            if (node.hasNode(component))
                componentNode = node.getNode(component);
            else
                componentNode = node.addNode(component);

            node = componentNode;
        }

        return node;
    }

    /**
     * Create a new node of the given name and nodeType at path.
     * 
     * @param path
     * @param name
     * @param nodeType
     * @return
     * @throws RepositoryException
     */
    public Node newNode(String path, String name, String nodeType) throws RepositoryException {
      Node node = getNode(session.getRootNode(), path);

        if (nodeType != null)
            node = node.addNode(name, nodeType);
        else
            node = node.addNode(name);

        node.getParent().save();

        return node;
    }

    public Node findLastExistingNode(String path) throws RepositoryException {
        path = normalizePath(path);
        Node root = getSession().getRootNode();

        while (path != null && path.length() > 0 && !root.hasNode(path)) { // find deepest existing node in path
            int separator = path.lastIndexOf("/");
            if (separator < 0)
                return null;
            path = path.substring(0, separator);
        }

        if (path == null || path.length() == 0)
            return null;

        Node node = root.getNode(path); // find deepest existing node in path
        return node;
    }

    public String getPropertyInPath(String path, String property) throws RepositoryException {
        Node node = findLastExistingNode(path);
        if (node == null)
            return null;

        while (node.getName().length() > 0 && !node.hasProperty(property)) {
            node = node.getParent();
        }

        if (node.getName().length() == 0)
            return null;

        return node.getProperty(property).getString();
    }

    public Node findTypeWithProperty(Node root, String nodeTypeName, String propertyName, String propertyValue) throws RepositoryException {
        if (root.isNodeType(nodeTypeName) && root.hasProperty(propertyName) && root.getProperty(propertyName).getString().equals(propertyValue))
            return root;

        NodeIterator ni = root.getNodes();
        while (ni.hasNext()) {
            Node found = findTypeWithProperty(ni.nextNode(), nodeTypeName, propertyName, propertyValue);
            if (found != null)
                return found;
        }

        return null;
    }

    /**
     * Convert the prefix name into a Name
     *
     * @param name the prefix name
     * @return the Name object
     */
    public Name getName(String name) {
        return NameFactoryImpl.getInstance().create(getNamespaces().toQNameString(name));
    }

    /**
     * Return the NodeTypeDef of the node type given as a prefix name.
     *
     * @param nodeType the name of the node type as a prefix name
     * @return the node type definition
     *
     * @throws NoSuchNodeTypeException
     */
    public NodeTypeDef getNodeTypeDef(String nodeType) throws NoSuchNodeTypeException {
        return getNodeTypeDef(getName(nodeType));
    }

   /**
     * Return the NodeTypeDef of the node type given as a prefix name.
     *
     * @param nodeType the name of the node type as a Name
     * @return the node type definition
     *
     * @throws NoSuchNodeTypeException
     */
    public NodeTypeDef getNodeTypeDef(Name nodeType) throws NoSuchNodeTypeException {
        SessionImpl s = (SessionImpl) getSession();
        NodeTypeManagerImpl nmgr = s.getNodeTypeManager();
        return nmgr.getNodeTypeRegistry().getNodeTypeDef(nodeType);
    }

    public Session getSession() {
        return session;
    }

    public void setSession(Session session) {
        this.session = session;
    }

    /**
     * Store a nodes:file node.
     *
     * @param node
     * @param name
     * @param contentType
     * @param content
     * @return
     * @throws RepositoryException
     */
    public Node storeBlob(Node node, String name, String contentType, byte[] content) throws RepositoryException {
        return storeBlob(node, name, contentType, content, null);
    }

    /**
     * Store a file node
     *
     * @param node
     * @param name
     * @param contentType
     * @param content
     * @param mixin
     * @return
     * @throws RepositoryException
     */
    public Node storeBlob(Node node, String name, String contentType, byte[] content, String mixin) throws RepositoryException {
        if (node.hasNode(name)) { // node exists already: overwrite
            Node old = node.getNode(name);
            NodeIterator i = old.getNodes();
            while (i.hasNext()) {
                Node child = i.nextNode();
                child.remove(); // remove children, add them newly below
            }
            old.remove();
        }

        Node fileNode = node.addNode(name, Constants.TYPE_NODES_FILE); // create new node

        if (mixin != null)
            fileNode.addMixin(mixin);
        Node resNode = fileNode.addNode(Constants.JCR_CONTENT, Constants.JCR_NT_RESOURCE);
        if (mixin != null)
            resNode.addMixin(mixin);
        if (contentType != null)
            resNode.setProperty(Constants.PROP_MIME_TYPE, contentType);
        else
            resNode.setProperty(Constants.PROP_MIME_TYPE, "application/octet-stream");
        resNode.setProperty(Constants.PROP_ENCODING, "");
        resNode.setProperty(Constants.PROP_DATA, new ByteArrayInputStream(content));
        Calendar lastModified = Calendar.getInstance();
        resNode.setProperty(Constants.PROP_LAST_MODIFIED, lastModified);

        return fileNode;
    }

    /**
     * Return the data property from a file node
     * 
     * @param file
     * @return
     * @throws RepositoryException
     */
    public Property getBlob(Node file) throws RepositoryException {
        if (!file.hasNode(Constants.JCR_CONTENT))
            return null;

        Node content = file.getNode(Constants.JCR_CONTENT);
        return content.getProperty(Constants.PROP_DATA);
    }

    /**
     * Update the content of a file node.
     *
     * @param file
     * @param value
     * @throws RepositoryException
     */
    public void updateBlob(Node file, byte[] value) throws RepositoryException {
        if (!file.hasNode(Constants.JCR_CONTENT))
            return;

        Node content = file.getNode(Constants.JCR_CONTENT);
        
        Calendar lastModified = Calendar.getInstance();
        content.setProperty(Constants.PROP_LAST_MODIFIED, lastModified);
        content.setProperty(Constants.PROP_DATA, new ByteArrayInputStream(value));
    }
}
